หาตั๋วเครื่องบินราคาถูกล่วงหน้าด้วย Javascript และ Puppeteer
Table of Contents
เราจะมีวิธีหาราคาตั๋วเครื่องบินดีๆแบบล่วงหน้าหลายเดือนได้ไหม วันนี้ผมขอทดลองใช้ javascript แล้วก็ Chrome headless อย่าง Puppeteer มาช่วยแก้ปัญหานี้ดูครับ
เริ่มจากหาแหล่งข้อมูล #
สำหรับข้อมูลผมหาได้ไม่ยากครับ ในหน้าเว็บของสายการบินมีเป็นปฏิทินให้อยู่แล้ว ผมแค่เข้าไปเลือกว่าจะเดินทางจากไหนไปไหน เท่านี้ก็จะมีราคาคร่าวๆให้เราได้ใช้กันแล้ว โดยในบทความนี้ผมจะเลือกเดินทางจาก “สกลนคร SNO” ไปลงที่ “ดอนเมือง DMK” เที่ยวเดียวนะครับ
สายการบินสีเหลือง #
{dateKey: "YYYYMMDD", amount: "X,XXX.XX"}
ใช่แล้วครับ มันคือวันที่และราคาค่าตั๋วที่ถูกที่สุดของวันนั้นนั่นเอง ถ้าเกิดลอง copy link address ไปเปิดใน tab ใหม่ก็จะได้สิ่งที่เราต้องการ
สายการบินสีแดง #
"YYYY-MM-DD": X,XXX.XX
ไม่พูดพร่ำทำเพลงอะไรมากครับ copy link ไปลองเปิดใน tab ใหม่ดูเหมือนเดิม แต่รอบนี้ไม่เหมือนเดิม…!!
{"message": "Unauthorized"}
ก็ได้ด้วยว่ะ แต่ด้วยความขี้เกียจเพราะเกรงว่าบทความนี้จะยาวเกินไป ผมจะไม่ไปงมหา apiKey
หรือ Token
อะไรให้เสียเวลา ผมจะใช้ Puppeteer นี่แหละดึงมันออกมาเอง เอาเป็นว่าเห็นละกันว่ามันอยู่ตรงไหนจบไปสำหรับแหล่งข้อมูลของสายการบินนี้ครับ
Coding #
เอาล่ะครับ ถึงเวลาที่จะต้องลงไม้ลงมือจัดการเอาข้อมูลมาจากแหล่งที่เราได้ไปสำรวจมาแล้วซะทีนะครับ โดยในบทความนี้ผมมีแผนการคร่าวๆดังนี้นะครับ
- ข้อมูลจากสายการบินเหลืองด้วยการยิง request ไปขอตรงๆ ซึ่งผมจะเลือกใช้ตัวช่วยก็คือ axios เพื่อมาช่วยยิง request แล้วก็จัดการ response นะครับ
- ข้อมูลจากสายการบินสีแดงอันนี้ยากขึ้นมาหน่อยเพราะจะยิงไปขอดุ่มๆก็ไม่ได้ แต่ผมเองก็ขี้เกียจไป Reverse Engineering API ของเว็บเขาอีกครับเพราะคิดว่าน่าจะแยกได้เป็นอีกหนึ่งบทความเลย ผมเลยจะใช้ความสามารถของ Puppeteer ครับ โดยเข้าไปที่หน้าเว็บแล้วก็ทำตัวเสมือนคนใช้ทั่วไปคือเลือกสนามบินแล้วค่อยมากดดูฏิทิน ใช้ความสามารถของ Puppeteer ในการดัก response ที่เกิดขึ้นใน page เพื่อหาชุดข้อมูลราคาออกมา
- เอาข้อมูลจากทั้ง 2 ที่มาจัด format ให้เหมือนกันแล้วก็ merge กันให้เรียบร้อย โดยส่วนของวันที่ผมจะใช้ dayjs มาช่วยจัด format แล้วก็ใช้เปรียบเทียบวันที่เวลาที่ต้องการ filter ราคาครับ
ดึงข้อมูลจากสายการบินสีเหลือง #
ตัวนี้ไม่ซับซ้อนครับ ผมเขียนเป็น function ที่ return promise ด้านในเรียกใช้ axios ไปที่ url แหล่งข้อมูลที่ดัดแปลงเรียบร้อยแล้ว
const getYellowAirlineFares = () => {
return new Promise(async (resolve, reject) => {
console.log('- Get flight from Yellow Airline');
const fromDate = dayjs().startOf('month').format('MM/DD/YYYY');
const toDate = dayjs().add(1, 'year').format('MM/DD/YYYY');
const mainUrl = 'https://www.nokair.com/Flight/GetCalendarFare';
const url = `${mainUrl}?from=${departure}&to=${destination}&fromDate=${fromDate}&toDate=${toDate}¤cy=THB`;
try {
const res = await axios.get(url);
const result = res.data.map(item => {
const itemDate = dayjs(item.dateKey, 'YYYYMMDD');
return {
date: itemDate.format('DD/MM/YYYY'),
day: itemDate.format('ddd'),
fare: parseFloat(item.amount.replace(',', '')),
brand: 'YELLOW'
};
})
resolve(result);
} catch (error) {
console.error(error);
reject(error);
}
});
};
จะเห็นว่าหลังจากคำสั่ง axios.get()
ผมเอา response ที่ได้มาทำการ map ใหม่เพื่อให้ได้หน้าตา object ตามต้องการแล้วเดี๋ยวอีกสายการบินก็จะทำเหมือนกันครับ
ดึงข้อมูลจากสายการบินสีแดง #
ของเจ้านี้จะค่อนข้างยาวขึ้นมาหน่อยเพราะเราไม่ได้ใช้การยิงตรงๆแบบเมื่อกี้ หน้าตา function ก็จะประมาณนี้ครับ ประกอบด้วยส่วนที่ setup ตัว puppeteer รวมไปถึงคำสั่งในการเข้าถึงข้อมูล
const getRedAirlineFares = () => {
return new Promise(async (resolve, reject) => {
console.log('- Get flight from Red Airline');
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
const mainUri = 'https://www.airasia.com/th/th';
const departSelector = 'input[aria-controls="home-origin-autocomplete-heatmapstation-combobox"]';
try {
console.log('go to page');
await page.goto(mainUri);
console.log('waiting for input field');
await page.waitForSelector(departSelector);
console.log('click on input field');
await page.click(departSelector);
console.log('input departure airport');
await page.keyboard.type(departure);
await page.waitForSelector('li[id="home-origin-autocomplete-heatmaplist-0"]');
await page.keyboard.press('Enter');
await page.waitFor(1000);
console.log('input destination airport');
await page.keyboard.type(destination);
await page.waitForSelector('li[id="home-destination-autocomplete-heatmaplist-0"]');
await page.waitFor(1000);
await page.keyboard.press('Enter');
} catch (error) {
browser.close();
console.error(error);
resolve([]);
}
page.on('response', async (response) => {
const pattern = /pricecalendar\/\d\/\d\/THB\/\w{3}\/\w{3}\/\d{4}-\d{2}-\d{2}\/1\/\d+/;
if (pattern.test(response.url())) {
console.log('detected', response.url());
const data = await response.json();
try {
const dataKey = `${departure}${destination}|THB`;
const result = Object.keys(data[dataKey]).map(key => {
const keyDate = dayjs(key, 'YYYY-MM-DD')
const date = keyDate.format('DD/MM/YYYY');
const day = keyDate.format('ddd');
const fare = data[dataKey][key];
return {
date, day, fare,
brand: 'RED'
};
});
browser.close();
resolve(result);
} catch (error) {
console.error(error);
browser.close();
reject(error);
}
}
});
});
};
ส่วนที่ใช้เปิดเว็บแล้วก็เลือกสนามบินด้วยการพิมพ์ครับ เลียนแบบการใช้งานปกติของตัวผมเองนี่แหละ
console.log('- Get flight from Red Airline');
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
const mainUri = 'https://www.airasia.com/th/th';
const departSelector = 'input[aria-controls="home-origin-autocomplete-heatmapstation-combobox"]';
try {
console.log('go to page');
await page.goto(mainUri);
console.log('waiting for input field');
await page.waitForSelector(departSelector);
console.log('click on input field');
await page.click(departSelector);
console.log('input departure airport');
await page.keyboard.type(departure);
await page.waitForSelector('li[id="home-origin-autocomplete-heatmaplist-0"]');
await page.keyboard.press('Enter');
await page.waitFor(1000);
console.log('input destination airport');
await page.keyboard.type(destination);
await page.waitForSelector('li[id="home-destination-autocomplete-heatmaplist-0"]');
await page.waitFor(1000);
await page.keyboard.press('Enter');
} catch (error) {
browser.close();
console.error(error);
resolve([]);
}
ส่วนที่ใช้เปิดเว็บแล้วก็เลือกสนามบินด้วยการพิมพ์ครับ เลียนแบบการใช้งานปกติของตัวผมเองนี่แหละ ต่อมาก็คือส่วนที่รอ response โดยผมเลือกรับเฉพาะ response จาก url เป้าหมายที่เราไปเรียกเองแล้วมันไม่ผ่านนั่นแหละครับ ซึ่งหลังจากได้ข้อมูลมาแล้วก็เอามาจัด format ให้เหมือนกัน
page.on('response', async (response) => {
const pattern = /pricecalendar\/\d\/\d\/THB\/\w{3}\/\w{3}\/\d{4}-\d{2}-\d{2}\/1\/\d+/;
if (pattern.test(response.url())) {
console.log('detected', response.url());
const data = await response.json();
try {
const dataKey = `${departure}${destination}|THB`;
const result = Object.keys(data[dataKey]).map(key => {
const keyDate = dayjs(key, 'YYYY-MM-DD')
const date = keyDate.format('DD/MM/YYYY');
const day = keyDate.format('ddd');
const fare = data[dataKey][key];
return {
date, day, fare,
brand: 'RED'
};
});
browser.close();
resolve(result);
} catch (error) {
console.error(error);
browser.close();
reject(error);
}
}
});
ฟังก์ชันสุดท้ายสำหรับรวมข้อมูลจาก 2 แหล่งแล้วเอามา filter ครับ โดยผมกำหนดไว้ว่าจะ “เอาแค่เที่ยวบินที่บินในเดือนนี้และเดือนหน้า ที่ราคาไม่เกิน 1,000 บาท” ครับ เสร็จแล้วก็ save ไปที่ไฟล์ชื่อ results.json
เลย
const getFares = async () => {
const red = await getRedAirlineFares();
const yellow = await getYellowAirlineFares();
const results = red.concat(yellow);
const nextTwoMonth = dayjs().add(2, 'month').startOf('month');
const filtered = results.filter(item => {
return dayjs(item.date, 'DD/MM/YYYY').isBefore(nextTwoMonth, 'date') && item.fare < 1000
})
const writeFile = util.promisify(fs.writeFile);
try {
await writeFile('results.json', JSON.stringify(filtered));
console.log(`founded ${filtered.length} in results.json`);
} catch (error) {
console.error(error);
}
};
เมื่อเรา run ก็จะได้ผลลัพธ์ประมาณนี้ครับ
> node index.js
- Get flight from Red Airline
go to page
waiting for input field
click on input field
input departure airport
input destination airport
detected https://k.airasia.com/availabledates-pwa/api/v1/pricecalendar/0/1/THB/SNO/DMK/2019-08-21/1/16
- Get flight from Yellow Airline
founded 63 in results.json
และเมื่อเราเปิดดูไฟล์ results.json
ก็จะได้ข้อมูลที่สวยงามรอให้เอาไปใช้งานต่อได้แล้วครับ

ตัวอย่าง code #
https://github.com/clonezer/get-flight-fares
จบไปแล้วครับสำหรับการทดลองหาตั๋วที่ราคาถูกล่วงหน้า จาก 2 สายการบินชื่อดังในประเทศของเราครับ ซึ่งก็ถือว่าประหยัดเวลาวางแผนไปได้พอสมควร แต่อยากจะฝากใครที่อ่านจบแล้วกำลังมีไอเดียอยากจะไปขุดเอาข้อมูลพวกนี้ไปใช้ทำมาหากิน อย่าลืมไปอ่าน policy ของเว็บด้วยนะครับว่าเขาอนุญาตให้เราเอาไปใช้ประโยชน์แบบไหนได้หรือเปล่า สำหรับกรณีนี้ผมใช้เพื่อศึกษาการเขียน code เท่านั้นนะครับ